/* Display.cpp
Copyright (c) 2008 Michael Zahniser
Please note the license terms (MIT license) at the end of this document.

Function definitions for the Display class.
*/

#include "Display.h"

#include "_sdl/EmbeddedFont.h"
#include "_common/shift.h"
#include "_sdl/PixMap.h"

#include <cstring>
#include <iostream>

using std::memcpy;
using std::memset;
using std::cerr;
using std::endl;



// Default constructor:
Display::Display()
{
	Initialize();
}



// Run:
void Display::Run()
{
	// Bail out if initialization failed.
	if(!window)
		return;
	
	// This array maps the numeric keypad values to ordinary key events.
	static const SDLKey keypad[17] = {SDLK_0, SDLK_1, SDLK_2, SDLK_3, SDLK_4, SDLK_5, SDLK_6, SDLK_7, SDLK_8, SDLK_9,
		SDLK_PERIOD, SDLK_SLASH, SDLK_ASTERISK, SDLK_MINUS, SDLK_PLUS, SDLK_RETURN, SDLK_EQUALS};
		
	while(true)
	{
		Draw();
		
		// Wait for an event that passes the filter.
		SDL_Event event;
		SDL_WaitEvent(&event);
		
		if(event.type == SDL_QUIT)
			break;
		else if(event.type == SDL_KEYDOWN)
		{
			SDLKey key = event.key.keysym.sym;
			SDLMod mod = event.key.keysym.mod;
			
			// Map keypad buttons to ordinary keys.
			if(key >= SDLK_KP0 && key <= SDLK_KP_EQUALS)
				key = keypad[key - SDLK_KP0];
			
			// Get the modifier key state.
			bool isCommand = (mod & (KMOD_CTRL | KMOD_META)) != 0;
			bool isShifted = (mod & KMOD_SHIFT) != 0;
			
			// Escape or ^Q quits.
			if(key == SDLK_ESCAPE || (isCommand && key == 'q'))
				break;
			else if(isCommand)
			{
				// The name of a command (control+X) is "^x".
				string command("^");
				command += key;
				
				calculator(command);
			}
			else if(key == SDLK_RETURN || key == ' ')
			{
				// Commands can be entered with the return key or with spaces.
				if(entry.empty())
					entry = calculator.Previous();
				
				if(entry == "quit")
					break;
				else if(entry == "calculatingColor")
					ReadColor(calculatingColor);
				else if(entry == "programmingColor")
					ReadColor(programmingColor);
				else if(entry == "definingColor")
					ReadColor(definingColor);
				else
					calculator(entry);
				entry.clear();
			}
			else if(key == SDLK_DELETE || key == SDLK_BACKSPACE)
			{
				// Shift + Delete clears the entry completely. Ordinary Delete clears one character.
				if(isShifted)
					entry.clear();
				else if(!entry.empty())
					entry.erase(entry.end() - 1, entry.end());
			}
			else if(key == SDLK_TAB)
			{
				// TODO: tab completion of names.
			}
			else if(key == SDLK_LEFT)
			{
				// TODO: show previous name.
			}
			else if(key == SDLK_RIGHT)
			{
				// TODO: show next name.
			}
			else if(key == SDLK_UP)
			{
				entry = calculator.Previous();
			}
			else if(key == SDLK_DOWN)
			{
				entry = calculator.Next();
			}
			else if(key >= ' ' && key <= '~')
			{
				// Enter text. Simulate the shift key.
				entry += isShifted ? SHIFT[key] : key;
			}
		}
	}
	
	SDL_Quit();
}



// Initialize:
void Display::Initialize()
{
	if(SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		cerr << "Unable to initialize SDL." << endl;
		return;
	}

	stackFont.Load(EmbeddedFont::DATA, EmbeddedFont::WIDTH, EmbeddedFont::HEIGHT);
	entryFont.Load(EmbeddedFont::DATA, EmbeddedFont::WIDTH, EmbeddedFont::HEIGHT);
	
	// Set the caption. The window itself has not title bar, but this also controls the name
	// shown on the Windows task bar, among other things.
	window.SetCaption("hsiloP");

	// Create the window.
	window.Create(WINDOW_WIDTH, LINE_HEIGHT);

	// RGB colors for the different elements of the UI.
	calculatingColor[0] = window.Color(100, 140, 60);
	calculatingColor[1] = window.Color(90, 130, 50);
	calculatingColor[2] = window.Color(30, 60, 10);
	
	programmingColor[0] = window.Color(140, 120, 40);
	programmingColor[1] = window.Color(130, 110, 30);
	programmingColor[2] = window.Color(60, 50, 10);
	
	definingColor[0] = window.Color(60, 120, 180);
	definingColor[1] = window.Color(50, 110, 170);
	definingColor[2] = window.Color(10, 50, 70);

	stackFont.SetColor(0, 0, 0);
	entryFont.SetColor(255, 255, 255);
	
	// Enable filtering and key repeats.
	SDL_SetEventFilter(&EventFilter);
	SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
}



// Draw:
void Display::Draw()
{
	UpdateWindow();
	
	const int padX = 4;
	const int padY = (LINE_HEIGHT - entryFont.Height()) / 2;
	
	double y = window.Height() - LINE_HEIGHT;
	
	// Draw the error message, if any.
	if(!calculator.Error().empty())
	{
		string error = calculator.Error();
		
		window.FillRect(0, y, window.Width(), LINE_HEIGHT, window.Color(0));

		int x = window.Width() - entryFont.Width(error) - padX;
		entryFont.Draw(error, window, x, y + padY);
		y -= LINE_HEIGHT;
	}
	
	// Draw the entry.
	{
		string text = calculator[0];
		
		State::EntryMode mode = calculator.GetState().Mode();
		Uint32 back = 
			(mode == State::CALCULATING) ? calculatingColor[2] : 
			(mode == State::PROGRAMMING) ? programmingColor[2] :
			(mode == State::DEFINING) ? definingColor[2] : window.Color(80);
		window.FillRect(0, y, window.Width(), LINE_HEIGHT, back);
		
		int x = padX;
		if(mode == State::DEFINING)
			x = (window.Width() - entryFont.Width(entry)) / 2;
		
		entryFont.Draw(entry, window, x, y + padY);
		y -= LINE_HEIGHT;
	}
	
	// Draw the lines, one by one.
	bool alternate = calculator.Size() & 1;
	for(int i = 0; y >= 0; ++i, y -= LINE_HEIGHT, alternate = !alternate)
	{
		string text = calculator[i];
		
		if(text == "{")
			DrawTransition(y, false,
				calculatingColor[alternate],
				programmingColor[alternate]);
		else if(text == "}")
			DrawTransition(y, true,
				programmingColor[alternate],
				definingColor[alternate]);
		else if(text == "=")
			DrawTransition(y, true,
				calculatingColor[alternate],
				definingColor[alternate]);
		else
		{
			// Draw the appropriate background.
			bool isProgram = (text[0] == ' ');
			Uint32 back = isProgram ? 
				programmingColor[alternate] : 
				calculatingColor[alternate];
			window.FillRect(0, y, window.Width(), LINE_HEIGHT, back);
			
			int x = padX;
			if(isProgram)
			{
				text = text.substr(1);
				x = (window.Width() - stackFont.Width(text)) / 2;
			}
		
			stackFont.Draw(text, window, x, y + padY);
		}
	}
	
	window.Update();
}



// UpdateWindow:
void Display::UpdateWindow()
{	
	// There is a limit to how big the window can grow.
	int rows = calculator.Size() + 1;
	if(rows > MAX_LINES)
		rows = MAX_LINES;
	rows += !calculator.Error().empty();
	
	// Resize the window, if necessary.
	window.Resize(WINDOW_WIDTH, rows * LINE_HEIGHT);
}



// Draw a transition from one color to another.
void Display::DrawTransition(int y, bool down, Uint32 from, Uint32 to)
{
	Surface block = window.Region(0, y, window.Width(), LINE_HEIGHT);
	block.Erase(block.Color(255));
	PixMap pix(block);
	
	double slope = -(LINE_HEIGHT - 1) / (window.Width() * .5);
	double cut = LINE_HEIGHT - 2 + slope * .5;
	if(down)
	{
		slope = -slope;
		cut = -1 + slope * .5;
	}
	
	for(int x = 0; x < (window.Width() + 1) / 2; ++x)
	{
		int y = 0;
		for( ; y < cut; ++y)
			pix(x, y) = pix(window.Width() - 1 - x, y) = from;
		
		double f = y - cut;
		pix(x, y) = pix(window.Width() - 1 - x, y) = Darken(from, 1. - f);
		++y;
		pix(x, y) = pix(window.Width() - 1 - x, y) = Darken(to, f);
		
		for(++y; y < LINE_HEIGHT; ++y)
			pix(x, y) = pix(window.Width() - 1 - x, y) = to;
		
		cut += slope;
	}
}



// Darken the given color.
Uint32 Display::Darken(Uint32 color, double f)
{
	Uint32 result = 0;
	for(int shift = 0; shift < 32; shift += 8)
		result += static_cast<Uint32>(((color >> shift) & 0xFF) * f) << shift;
	return result;
}



// Read an entry in the given color array.
void Display::ReadColor(Uint32 *color)
{
	calculator.CopyState();
	int index = calculator.GetState().PopInt();
	if(index < 0 || index > 2)
		index = 0;
	ReadColor(color[index]);
}



void Display::ReadColor(Uint32 &color)
{
	int blue = calculator.GetState().PopInt();
	int green = calculator.GetState().PopInt();
	int red = calculator.GetState().PopInt();
	
	color = window.Color(red, green, blue);
}



// EventFilter:
// Filter that rejects all events not used by this application.
int Display::EventFilter(const SDL_Event *event)
{
	return (event->type == SDL_QUIT) | (event->type == SDL_KEYDOWN);
}



/* Copyright (c) 2008 Michael Zahniser

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE. */
